Repository navigation
perf(rknn): price a ladder engine by what the driver reports - #970
Conversation
The flow-estimator ladder pins its resident rungs with a linear proxy, 5 MiB per compiled frame, because the cost of a context is not its image size (all six estimator engines are 661-714 MB and occupy 0.27-3.7 GiB). The proxy is a guess with a shape it inherits from the long rungs: it over-pins the short ones by up to 11x (S=110 really costs 786 MiB, the proxy charges 550) and it refuses sets the board admits, so a ladder that could hold two rungs at once re-inits one per request. Read the number from the driver instead: `rknn_init` with `RKNN_FLAG_COLLECT_MODEL_INFO_ONLY` parses the image without building the network (~0.5 s per engine, no NPU memory allocated) and `RKNN_QUERY_MEM_SIZE` then reports `total_weight_size + total_internal_size`, the bytes the real init goes on to allocate, less a per-context slack of 30-50 MiB. `engine::rknn::engine_memory_mib` does that once per engine at registration and returns 0 when the driver cannot say, which keeps the proxy as the fallback. `ApertureBudget` therefore counts MiB, its only unit, and the ceiling this board fits is 3900 MiB: 110+200+250 (3341), 110x4 (3144), 200x3 (3387), 250+250+110 (3638), 110+400 (3401) and 200+400 (3744) initialize together in one process, while 250+400 (4041), 110+500 (4538) and every set holding an S>=900 engine fail at `rknn_init` with -4. Measured on RK3588 with the six-rung ladder and six requests alternating between the 200 and 400 rungs: the proxy pins one rung at a time and re-inits on every length change (6 `rknn_init` calls, 154.9 s of request wall time, peak RSS 5.09 GiB), the measured cost keeps 200+400 co-resident at 3744 MiB (2 loads, 141.7 s, -8.5%, peak RSS 6.20 GiB - the second engine's residency, paid for the loads). The served audio is bit-identical between the runs (same engines, same padded runs).
|
@wOvAN The PR undercounts engine memory because it adds two 32-bit byte counts before widening the result. Example: 1 GiB + 3.5 GiB = 4.5 GiB, but the addition may overflow and reports 512 MiB. |
`rknn_mem_size` carries `total_weight_size` and `total_internal_size` as `uint32_t` (rknn_api.h 2.3.2), so adding them and then casting widened the result of a sum that had already wrapped: a 1 GiB weight set beside a 3.5 GiB workspace reports 512 MiB. Widen the first operand, then add and round. The table this branch ships is unaffected: the largest sum on the served ladder is S=500 at 668604160 + 3265074496 = 3933678656 bytes, under the 32-bit ceiling, so 786/1129/1426/2615/3752/1039 MiB stand. The wrap bites above 4 GiB, which is where the S>=1100 engines sit.
|
Confirmed, thanks — Fixed in The shipped table is unaffected, so the board evidence stands — the largest sum on the served ladder is S=500 at |
|
@wOvAN Thanks for the quick turnaround! PR merged! |
…ke-word/Whistle NEON + Drama3 deepening + per-model server device selection Upstream commits a8ba5a3..5074956 (16): - shared object libraries declared in model registrations (0xShug0#966) - OpenWakeWord Alexa GGUF package (0xShug0#968) - Whistle host decoder restored + opt-in NEON acceleration (0xShug0#967) - opt-in NEON network execution for microWakeWord (0xShug0#969) - fish_drama3: per-layer expert counts, speech-only head (0xShug0#959) - server: exec_plan engines counted in free-memory guard (0xShug0#964) - server: config model settings kept on WebUI loads, demo voices with --ui (0xShug0#971) - fix(rknn): exec_plan engine lookup next to the model (0xShug0#963) - webui: configured models treated same with/without UI management (0xShug0#972) - perf(rknn): ladder engine priced by driver report (0xShug0#970) - feat(server): per-model backend device selection (0xShug0#973) - perf(fish_dac): chunked reference encoding cuts VRAM peak (0xShug0#960) - webui: Vietnamese interface translations (0xShug0#839) - Drama3 base/small release cleanup (0xShug0#976) - text chunking: closing quotes kept with their sentence, tag-aware (0xShug0#977) - fish_drama3: default top_k 4097 no cap, matching Fish's Space (0xShug0#978) # Conflicts: # src/community_models/whistle_asr/session.cpp
What changes
The CosyVoice3 flow-estimator ladder decides which compiled engines it keeps resident with a linear proxy: 5 MiB per compiled frame, against a 2800 MiB ceiling. The proxy exists because the cost of a context is not its image size — all six estimator engines are 661-714 MB on disk and occupy 0.27-3.7 GiB in the NPU I/O aperture — but the proxy is a guess that inherits its shape from the long rungs, and it prices the ladder wrong in both directions:
Two consequences. The cost of a rung is not a function of its compiled length (S=900 is cheaper than S=500), and the proxy's over-estimate on the short rungs makes the ladder refuse sets the board admits:
200+400costs 3000 MiB by the proxy, so the ladder evicts the short rung, while the board initializes that pair in one process at 3744 MiB. A ladder that could hold two rungs re-inits one of them on every length change — ~2.8 s ofrknn_initper request.How the number is read
engine::rknn::engine_memory_mib(path, label):rknn_initwithRKNN_FLAG_COLLECT_MODEL_INFO_ONLYparses the image without building the network (~0.5 s per engine, no NPU memory allocated), andrknn_query(RKNN_QUERY_MEM_SIZE)then reportstotal_weight_size + total_internal_size— the bytes the real init goes on to allocate, less a per-context slack of 30-50 MiB measured on this board. It returns 0 when the driver cannot say, so each sidecar keeps its own proxy as the fallback. Called once per engine at ladder registration, not per request.ApertureBudgetnow counts MiB, its only unit, and keeps no ceiling of its own; the ceiling stays model-local with the matrix it was derived from (kAperturePinBudgetMiB = 3900inrknn_flow.cpp).Runtime validation (RK3588, Orange Pi 5 Plus, 8 GiB,
librknnrt2.3.2)Board build:
cmake --build /root/audio.cpp/build -j 8 --target audiocpp_serverwith-DENGINE_ENABLE_RKNN=ON -DENGINE_RKNN_INCLUDE_DIR=/usr/local/include -DENGINE_RKNN_LIBRARY=/usr/lib/librknnrt.so, model setpiper_tts,kokoro_tts,qwen3_tts,cosyvoice3,f5_tts,voxcpm2.Two binaries from one tree —
main(bin-proxy) and this branch (bin-measured) — one server config listing all six rungs(
/root/srvconc/server-rknn-full.json,cosyvoice3.exec_plan→flow_estimator_seq{110,200,250,400,500,900}.rknnfrom
/root/audio.cpp/models/CosyVoice3-RKNN/), GGUF/root/audio.cpp/models/CosyVoice3-GGUF/cosyvoice3-q8_0.gguf, runner/root/pinladder/ab9-run.sh: cold server, sixPOST /v1/audio/speech(
--backend cpu --threads 4, zero-shot voice from/root/srvconc/ref.wav,seed=1234) alternatingbetween the S=200 and S=400 rungs (
n=4,50,10,50,4,50chars → 118/308/158/308/118/308 mel frames).mainrknn_initcalls per runresident_mib 3744)The saving is exactly one
rknn_initper request that needs no new engine; the price is the second engine's residency, visible in RSS. Registration logs the table (cosyvoice3.flow.dit.rknn.aperture_mib= 786/1129/1426/2615/3752/1039 on this board) andresident_mibthe live set.Ceiling evidence, same board, sets initialized in one process (
/root/memq/pairs.c): 110+200+250 (3341 MiB), 110×4 (3144), 200×3 (3387), 250+250+110 (3638), 110+400 (3401) and 200+400 (3744) allrknn_initcleanly, while 250+400 (4041), 110+500 (4538) and every set holding an S≥900 engine return -4. 3900 MiB is that measured boundary, not a board property — the accounting is shared, the numbers are this engine's.Trace showing the policy difference:
Host
rknn_engine_testpasses (ENGINE_BUILD_EXTENDED_TESTS=ON; the suite is registered under that gate): the served ladder trace re-run with the measured costs gives 2 loads and a 2555 MiB resident set,200+400fits,250+400(4041) and110+500(4538) do not, and S=900's own 1039 MiB fits the aperture while the engine still fails at its own IOVA ceiling.python3 tools/check_loader_catalog_sync.py --self-test && python3 tools/check_loader_catalog_sync.py→ok.Known limits
engine_memory_mibcosts ~0.5 s per engine at startup (3 s for a six-rung ladder), once, in exchange for the loads it removes per request.